using System;
using System.Collections.Generic;
using System.Linq;
using Microsoft.Xna.Framework;
using Microsoft.Xna.Framework.Audio;
using Microsoft.Xna.Framework.Content;
using Microsoft.Xna.Framework.GamerServices;
using Microsoft.Xna.Framework.Graphics;
using Microsoft.Xna.Framework.Input;
using Microsoft.Xna.Framework.Media;

namespace jvox
{
    /// <summary>
    /// This is the main type for your game
    /// </summary>
    public class Game1 : Microsoft.Xna.Framework.Game
    {
        GraphicsDeviceManager graphics;
        SpriteBatch spriteBatch;

        FirstPersonCamera camera;

        BasicEffect effect;
        Effect pixelshader;
        World world;

        const int RAYCASTWIDTH = 320;
        const int RAYCASTHEIGHT = 200;
        const int SCREENWIDTH = 800;
        const int SCREENHEIGHT = 600;

        RenderTarget2D rTarget;
        Texture2D renderTexture;

        Texture2D whiteTex;

        Texture2D testTexture;
        Texture2D rays;

        bool debugRender;

        public Game1()
        {
            graphics = new GraphicsDeviceManager(this);
            graphics.PreferredBackBufferHeight = SCREENHEIGHT;
            graphics.PreferredBackBufferWidth = SCREENWIDTH;
            Content.RootDirectory = "Content";
        }

        /// <summary>
        /// Allows the game to perform any initialization it needs to before starting to run.
        /// This is where it can query for any required services and load any non-graphic
        /// related content.  Calling base.Initialize will enumerate through any components
        /// and initialize them as well.
        /// </summary>
        protected override void Initialize()
        {
            world = new World();

            base.Initialize();
        }

        /// <summary>
        /// LoadContent will be called once per game and is the place to load
        /// all of your content.
        /// </summary>
        protected override void LoadContent()
        {
            // Create a new SpriteBatch, which can be used to draw textures.
            spriteBatch = new SpriteBatch(GraphicsDevice);
            camera = new FirstPersonCamera(this, Vector3.Zero);
            this.Components.Add(camera);
            effect = new BasicEffect(GraphicsDevice);
            rTarget = new RenderTarget2D(GraphicsDevice, RAYCASTWIDTH, RAYCASTHEIGHT, false, SurfaceFormat.Color, DepthFormat.Depth24); ;
            renderTexture = new Texture2D(GraphicsDevice, RAYCASTWIDTH, RAYCASTHEIGHT);
            whiteTex = Content.Load<Texture2D>("white");
            testTexture = new Texture2D(GraphicsDevice, 1024, 1024, false, SurfaceFormat.Single);
            pixelshader = Content.Load<Effect>("voxshader");

            //Test data
            float[] testColors = new float[testTexture.Width * testTexture.Height];
            testColors[0] = 0f;
            testColors[1] = 0f;
            testColors[2] = 0f;
            testColors[3] = 0f;
            testColors[4] = 0f;
            testColors[5] = 0f;
            testColors[6] = 0f;
            testColors[7] = 0f;
            testColors[8] = 1.0f;
            testColors[9] = 1.0f;
            testColors[10] = 0.0f;
            testColors[11] = 0.0f;
            testColors[12] = 0.0f;
            testColors[13] = 0.0f;
            testColors[14] = 64.0f;
            testColors[15] = 64.0f;
            testColors[16] = 64.0f;
            testTexture.SetData(testColors);

            rays = new Texture2D(GraphicsDevice, RAYCASTWIDTH * 2, RAYCASTHEIGHT, false, SurfaceFormat.Vector4);
        }

        /// <summary>
        /// UnloadContent will be called once per game and is the place to unload
        /// all content.
        /// </summary>
        protected override void UnloadContent()
        {
            // TODO: Unload any non ContentManager content here
        }

        /// <summary>
        /// Allows the game to run logic such as updating the world,
        /// checking for collisions, gathering input, and playing audio.
        /// </summary>
        /// <param name="gameTime">Provides a snapshot of timing values.</param>
        protected override void Update(GameTime gameTime)
        {
            // Allows the game to exit
            KeyboardState keyboard = Keyboard.GetState();
            if (keyboard.IsKeyDown(Keys.Escape))
                this.Exit();
            if (keyboard.IsKeyDown(Keys.D1))
                camera.moveSpeed = 30.0f;
            if (keyboard.IsKeyDown(Keys.D2))
                camera.moveSpeed = 3.0f;
            if (keyboard.IsKeyDown(Keys.D3))
                camera.moveSpeed = .3f;
            this.debugRender = keyboard.IsKeyDown(Keys.U);
            base.Update(gameTime);
        }

        /// <summary>
        /// This is called when the game should draw itself.
        /// </summary>
        /// <param name="gameTime">Provides a snapshot of timing values.</param>
        protected override void Draw(GameTime gameTime)
        {
            Window.Title = "" + OctreeNode.numVoxels + " voxels, camera at" + camera.position + "," + camera.updownRot;

            GraphicsDevice.BlendState = BlendState.Opaque;
            GraphicsDevice.SamplerStates[0] = SamplerState.LinearWrap;
            GraphicsDevice.DepthStencilState = DepthStencilState.Default;

            GraphicsDevice.SetRenderTarget(rTarget);

            GraphicsDevice.Clear(Color.CornflowerBlue);
            if (debugRender)
            {
                spriteBatch.Begin();
                for (int x = 0; x < rTarget.Width; x++)
                    for (int y = 0; y < rTarget.Height; y++)
                    {
                        Vector3 near, far;
                        near = new Vector3(x, y, 0);
                        far = new Vector3(x, y, 1);

                        //Get ray
                        near = GraphicsDevice.Viewport.Unproject(near, camera.projection, camera.view, Matrix.Identity);
                        far = GraphicsDevice.Viewport.Unproject(far, camera.projection, camera.view, Matrix.Identity);
                        Ray ray = new Ray(camera.position, far - near);
                        Color? res = world.root.GetIntersectingVoxel(ref ray);
                        if (res != null)
                        {
                            spriteBatch.Draw(whiteTex, new Rectangle(x, y, 1, 1), (Color)res);
                        }
                    }
                spriteBatch.End();
            }
            else
            {
                spriteBatch.Begin(0, BlendState.AlphaBlend, null, null, null, pixelshader);
                Matrix projection = Matrix.CreateOrthographicOffCenter(0, GraphicsDevice.Viewport.Width, GraphicsDevice.Viewport.Height, 0, 0, 1);
                Matrix halfPixelOffset = Matrix.CreateTranslation(-0.5f, -0.5f, 0);
                pixelshader.Parameters["MatrixTransform"].SetValue(halfPixelOffset * projection);
                pixelshader.Parameters["octreeWidth"].SetValue(testTexture.Width);
                pixelshader.Parameters["octreeHeight"].SetValue(testTexture.Height);
                pixelshader.Parameters["octree"].SetValue(testTexture);
                pixelshader.Parameters["screenWidth"].SetValue(GraphicsDevice.Viewport.Width);
                pixelshader.Parameters["screenHeight"].SetValue(GraphicsDevice.Viewport.Height);
                Ray[] rays = new Ray[GraphicsDevice.Viewport.Width * GraphicsDevice.Viewport.Height];
                for (int y = 0; y < rTarget.Height; y++)
                {
                    for (int x = 0; x < rTarget.Width; x++)
                    {
                        Vector3 near, far;
                        near = new Vector3(x, y, 0);
                        far = new Vector3(x, y, 1);

                        //Get ray
                        near = GraphicsDevice.Viewport.Unproject(near, camera.projection, camera.view, Matrix.Identity);
                        far = GraphicsDevice.Viewport.Unproject(far, camera.projection, camera.view, Matrix.Identity);
                        Ray ray = new Ray(camera.position, far - near);
                        ray.Direction.Normalize();
                        rays[x + rTarget.Width * y] = ray;
                    }
                }
                //Transfer to Vec4
                Vector4[] vec4 = new Vector4[GraphicsDevice.Viewport.Width * 2 * GraphicsDevice.Viewport.Height];
                for (int i = 0; i < rays.Length; i++)
                {
                    vec4[2 * i] = new Vector4(rays[i].Position, 0);
                    vec4[(2 * i) + 1] = new Vector4(rays[i].Direction, 0);
                }
                //Set texture data
                GraphicsDevice.Textures[2] = null;
                this.rays.SetData(vec4);
                pixelshader.Parameters["rays"].SetValue(this.rays);

                spriteBatch.Draw(whiteTex, new Rectangle(0, 0, rTarget.Width, rTarget.Height), Color.White);
                spriteBatch.End();
            }
            GraphicsDevice.SetRenderTarget(null);

            GraphicsDevice.Clear(Color.CornflowerBlue);

            GraphicsDevice.Textures[0] = null;
            Color[] texData = new Color[rTarget.Width * rTarget.Height];
            rTarget.GetData(texData);
            renderTexture.SetData(texData);

            spriteBatch.Begin(0, BlendState.Opaque, null, null, null);
            spriteBatch.Draw(renderTexture, new Rectangle(0, 0, GraphicsDevice.Viewport.Width, GraphicsDevice.Viewport.Height), Color.White);
            spriteBatch.End();

            base.Draw(gameTime);
        }
    }
}
